feat: "Connect with Tolgee" OAuth login for in-context editing - #39
feat: "Connect with Tolgee" OAuth login for in-context editing#39bdshadow wants to merge 8 commits into
Conversation
Adds a browser-redirect OAuth 2.1 login alongside the existing API-key path: - oauth/: PKCE (S256) via Web Crypto, authorization-code flow through chrome.identity.launchWebAuthFlow, token exchange + refresh, and a per-backend token store in the service worker. - background: OAUTH_LOGIN/OAUTH_GET_TOKEN/OAUTH_LOGOUT handlers plus a chrome.alarms routine that proactively refreshes rotating tokens and pushes the new access token into matching tabs without reloading. - content: injects the access token as __tolgee_authToken into page sessionStorage (the refresh token never leaves the service worker) and updates it in place on refresh. - popup: a "Connect with Tolgee" button; OAuth sessions persist only a marker + backend url and re-fetch a fresh token on open, so a short-lived token is never stored stale. - manifest: adds the "identity" and "alarms" permissions.
Make OAuth the primary sign-in: the "Connect with Tolgee" button sits directly under the API url, and the API-key input + Apply are tucked into a collapsible "API key sign in" block (collapsed by default). Also surface OAuth login failures via console.error instead of swallowing them.
OAuth access tokens carry no embedded project (unlike a PAK), so the popup now resolves one: it hints the page's configured project on connect, reads the consented project back from the token's tg.prj and injects it into the page as __tolgee_projectId, and shows a manual project picker only when the token is bound to all projects.
Gives the unpacked extension a stable, deterministic id so its chromiumapp.org redirect uri can be registered on the backend for local and preview OAuth testing.
…nnect launchWebAuthFlow steals focus and closes the popup, so the popup's post-login SET_CREDENTIALS never ran and the page never received the token (users had to inject it by hand). The service worker now pushes the full credential set (apiUrl, authToken, projectId) to the originating tab as soon as login resolves, independent of the popup's lifecycle. The content script reloads only when a value actually changed, so a redundant push doesn't reload the page twice.
The OAuth (Login) path now resolves the project the page declares against the connected server and injects it, or shows a clear "you can't edit this project here" error when it isn't accessible — instead of leaving the token unscoped and failing in-context with project_not_selected. Extract the reducer into a pure factory, cover it and the helpers with vitest, and run the tests in CI.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
WalkthroughThe extension adds PKCE OAuth login, token refresh, session persistence, credential propagation, project scoping, popup authentication flows, reducer state management, and Vitest coverage. ChangesOAuth authentication flow
Estimated code review effort: 4 (Complex) | ~60 minutes Mergeability Score: 🔵 Low · up to The OAuth login can fail without telling the user why, and an unvalidated server address may produce malformed or unsafe navigation. The PR is mergeable with explicit owner awareness and follow-up on these bounded issues. Sequence Diagram(s)sequenceDiagram
participant Popup
participant Background
participant OAuthClient
participant TokenEndpoint
participant ContentScript
Popup->>Background: Request OAuth login
Background->>OAuthClient: Start PKCE login
OAuthClient->>TokenEndpoint: Exchange authorization code
TokenEndpoint-->>OAuthClient: Return OAuth tokens
OAuthClient-->>Background: Return OAuth tokens
Background->>ContentScript: Inject access token and project ID
ContentScript-->>Popup: Apply updated credentials
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (7)
src/background/background.ts (1)
75-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueLog the messaging failure instead of discarding it.
The
catchswallows every error. If the content script is not present, login appears to succeed but no credentials reach the page, and nothing records why.Keep the non-throwing behavior, and log the reason.
🛠️ Proposed change
await browser.tabs .sendMessage(tabId, { type: 'SET_CREDENTIALS', data }) - .catch(() => undefined); + .catch((e) => + console.debug('[tolgee-oauth] credential injection skipped', tabId, e) + );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/background/background.ts` around lines 75 - 77, Update the sendMessage error handler in the background messaging flow to capture the caught error and log its reason, while preserving the existing non-throwing behavior and undefined fallback.src/oauth/tokenStore.ts (1)
7-12: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueDocument the storage choice for refresh tokens.
browser.storage.localpersists unencrypted on disk and survives browser restarts. It holds the refresh token here. That is a deliberate trade-off for long-lived sessions, but it differs frombrowser.storage.session, which stays in memory.Record the reason in the comment, and confirm the platform enforces an absolute refresh-token lifetime so a stale on-disk token cannot be replayed indefinitely.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/oauth/tokenStore.ts` around lines 7 - 12, Update the comment above saveSession to document that browser.storage.local stores refresh tokens unencrypted on disk across browser restarts as a deliberate long-lived-session trade-off versus browser.storage.session, and state the platform’s enforced absolute refresh-token lifetime that prevents indefinite replay of stale tokens.src/constants.ts (1)
14-25: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winConsider narrowing the requested scopes.
The access token is written into page-accessible storage (
AUTH_TOKEN_LOCAL_STORAGE). Any script running on that page can read it. Backend intersection with user permissions prevents privilege escalation, but it does not limit what a hostile page script can do with the token inside the user's own rights.screenshots.deleteandkeys.editare destructive.Request only the scopes the in-context editor actually calls, or request scopes incrementally.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/constants.ts` around lines 14 - 25, Update OAUTH_SCOPES to include only the permissions directly required by the in-context editor, removing destructive scopes such as screenshots.delete and keys.edit unless their corresponding operations are actually invoked. If those operations are needed conditionally, request their scopes incrementally rather than in the default token scope set.src/oauth/oauthClient.ts (1)
31-39: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winDo not put the raw response body in the error, and add a request timeout.
The thrown message embeds the full token-endpoint body.
src/background/background.ts(Lines 45-46) logs that error and returnsString(e)to the popup. A token endpoint can echo request parameters in an error body, so a refresh token or authorization code can reach the console and the popup.
fetchalso has noAbortSignal.apiUrlis user-supplied, so a non-responsive host stalls the login and refresh paths.🛠️ Proposed fix
const res = await fetch(`${base}/oauth2/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(params), + signal: AbortSignal.timeout(15_000), }); if (!res.ok) { - const body = await res.text().catch(() => ''); - throw new Error(`Tolgee token endpoint returned ${res.status}: ${body}`); + const body = await res.json().catch(() => null); + const reason = body && typeof body.error === 'string' ? body.error : ''; + throw new Error( + `Tolgee token endpoint returned ${res.status}${reason ? `: ${reason}` : ''}` + ); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/oauth/oauthClient.ts` around lines 31 - 39, Update the token request in the OAuth client’s fetch flow to avoid including the raw response body in thrown errors; report only the safe HTTP status or a generic failure message. Add an AbortSignal-based timeout to the fetch request, using the project’s established timeout convention if available, so user-supplied apiUrl hosts cannot stall login or refresh indefinitely.src/popup/storage.ts (1)
25-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReuse the shared
Valuestype instead of the local intersection.
src/popup/tools.tsalready declaresValueswithauthTokenandprojectId. This file keeps a second localValuesand then widens it with& { authToken?: string }. The two definitions can drift, and a reader cannot tell which one is authoritative. Import the type from./toolsand delete the local copy.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/popup/storage.ts` around lines 25 - 39, Update storeValues to import and use the shared Values type from ./tools directly, removing the local Values definition and the authToken intersection. Preserve the existing optional values handling and storage behavior.src/popup/useDetectorForm.tsx (1)
245-296: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueAdd the resolution inputs to the dependency array.
The effect reads
libConfig?.config?.projectId,checkableValues.apiUrl, andcheckableValues.authToken, but it only depends onstate.credentialsCheck. If the page reports a newprojectIdthroughTOLGEE_CONFIG_LOADEDwhilecredentialsCheckstays the same object, the popup keeps the previously resolved project. Add the read values to the dependency list.♻️ Proposed dependency change
- }, [state.credentialsCheck]); + }, [ + state.credentialsCheck, + (libConfig?.config as { projectId?: number | string } | undefined) + ?.projectId, + checkableValues?.apiUrl, + checkableValues?.authToken, + ]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/popup/useDetectorForm.tsx` around lines 245 - 296, Update the dependency array of the project-resolution useEffect to include libConfig?.config?.projectId and the read checkableValues.apiUrl and checkableValues.authToken values alongside state.credentialsCheck, so resolution reruns when any input changes.src/popup/sendToBackground.ts (1)
4-6: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winHandle
sendMessagerejections in this helper.
browser.runtime.sendMessagerejects when the service worker does not answer, for example after a worker restart or when the message channel closes. The callers (handleConnectinsrc/popup/TolgeeDetector.tsxandonLibConfigChangeinsrc/popup/useDetectorForm.tsx) do not catch the rejection, so the popup produces an unhandled rejection and the user sees no feedback. Return a normalized error result here, or addcatchat every call site.♻️ Proposed helper change
-export const sendToBackground = async (type: string, data?: any) => { - return browser.runtime.sendMessage({ type, data }); -}; +export const sendToBackground = async (type: string, data?: any) => { + try { + return await browser.runtime.sendMessage({ type, data }); + } catch (e) { + console.error(e); + return { error: String(e) }; + } +};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/popup/sendToBackground.ts` around lines 4 - 6, Update sendToBackground to catch rejections from browser.runtime.sendMessage and return a normalized error result instead of allowing the promise rejection to propagate. Preserve the existing message payload and successful response behavior, using the helper’s return contract so callers such as handleConnect and onLibConfigChange receive a consistent failure result.
🔇 Additional comments (24)
src/popup/reducer.test.ts (1)
1-207: LGTM!src/popup/tools.test.ts (1)
1-123: LGTM!vitest.config.ts (1)
1-10: LGTM!package.json (1)
19-21: LGTM!Also applies to: 51-52
.github/workflows/test.yml (1)
33-35: LGTM!src/constants.ts (2)
4-11: LGTM!
12-13: 🩺 Stability & Availability | ⚡ Quick winVerify the refresh skew against the backend access-token lifetime.
OAUTH_REFRESH_SKEW_MSis 60s, but the background refresh alarm insrc/background/background.ts(Line 82) runs every 10 minutes. If the backend issues access tokens with a lifetime under about 11 minutes, a token can expire in the page before the next alarm fires. The page-side token is only rotated by that alarm.Confirm the platform access-token TTL, then align the alarm period with it (for example, period ≤ TTL/2) or derive the period from
expiresAt.src/oauth/pkce.ts (1)
3-23: LGTM!src/oauth/oauthClient.ts (1)
84-96: LGTM!src/oauth/tokenStore.ts (1)
26-31: LGTM!manifest.json (2)
5-5: 🩺 Stability & AvailabilityConfirm the pinned key matches the Web Store item.
The
keyfield pins the extension ID soidentity.getRedirectURL()stays stable, which the OAuth redirect URI depends on. The value is a public key, so publishing it is safe.The key must match the one the Chrome Web Store assigned to this item. If it does not match, the store-installed build gets a different ID and the pre-registered redirect URI stops matching. Confirm the value against the store listing, and confirm Firefox builds do not need this field.
18-18: LGTM!src/background/background.ts (2)
4-16: LGTM!
100-113: 🩺 Stability & Availability
⚠️ Unverified finding
Sandbox verification was unavailable.Verify the content script rejects tokens for a non-matching
apiUrl.
browser.tabs.query({})returns every tab. This function then sends the OAuth access token to each one. The token reaches every tab that runs the content script, including tabs where the user never applied Tolgee credentials.The design depends on the content script comparing
data.apiUrlagainst the backend already applied in that tab, and discarding the message otherwise.src/content/contentScript.tsis not in this review context, so that guard is not confirmed here.Confirm the guard exists and compares origins, not raw strings. Alternatively, track which tabs received credentials and send only to those.
src/content/contentScript.ts (2)
4-6: LGTM!Also applies to: 18-37
84-110: LGTM!src/popup/tools.ts (1)
5-16: LGTM!Also applies to: 18-19, 25-32, 38-61
src/popup/storage.ts (1)
7-11: LGTM!Also applies to: 66-67
src/popup/reducer.ts (2)
1-72: LGTM!
156-244: LGTM!src/popup/useDetectorForm.tsx (1)
6-17: LGTM!Also applies to: 75-90, 133-192
src/popup/TolgeeDetector.tsx (3)
1-37: LGTM!Also applies to: 48-68
122-205: LGTM!Also applies to: 207-243, 269-281
284-358: LGTM!Also applies to: 375-421, 423-485
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/background/background.ts`:
- Around line 49-56: Update the OAUTH_GET_TOKEN and OAUTH_LOGOUT handlers to
attach rejection handlers to their promises, ensuring sendResponse is invoked
with an error response when getValidAccessToken or clearSession rejects,
including malformed apiUrl and storage failures. Preserve the existing success
responses and return true behavior, matching the error-handling pattern used by
OAUTH_LOGIN.
- Around line 80-97: Update the top-level alarm setup around REFRESH_ALARM to
first check whether the alarm already exists, creating it with the 10-minute
period only when absent. Keep the browser.alarms.onAlarm listener registration
at the top level and preserve its existing refresh handling.
In `@src/content/contentScript.ts`:
- Around line 112-117: Update the UPDATE_AUTH_TOKEN listener to validate that
data.authToken is present and non-empty before calling sessionStorage.setItem;
retain the same-origin check and skip the write when the token is missing so an
invalid "undefined" value cannot reach the SDK.
In `@src/oauth/oauthClient.ts`:
- Around line 16-24: Update parseTokenResponse to validate that
data.access_token is present and reject the response before constructing
OAuthTokens when it is missing. Replace the zero-second expires_in fallback with
a conservative default lifetime so getValidAccessToken does not treat tokens
with omitted expiry as immediately expired; preserve the existing refresh-token
fallback behavior.
- Around line 61-73: Update the authorization flow around authorizeUrl and
redirectResponse to retain the generated state, parse the redirect URL once, and
reject responses whose returned state differs or is missing. Before requiring
code, detect redirect error and error_description parameters and throw an error
that includes the provider’s failure reason; preserve the existing missing-code
validation for responses without either a code or provider error.
In `@src/oauth/tokenStore.ts`:
- Around line 35-57: Update getValidAccessToken to deduplicate concurrent
refreshes by caching one in-flight refresh promise per apiUrl, reusing it for
overlapping callers, and removing it when settled. Preserve existing
token/session behavior, but only clear the session when refresh reports an
authentication failure; propagate or retain the session for other errors instead
of treating every failure as invalid credentials.
In `@src/popup/reducer.ts`:
- Around line 131-155: Update the APPLY_VALUES case in the reducer so
appliedValues and storedValues preserve the existing OAuth authToken and
projectId alongside apiKey, apiUrl, and the conditionally effective branch.
Ensure the OAuth values remain intact when APPLY_VALUES is triggered through the
Server field without changing the existing branchEnabled behavior.
In `@src/popup/TolgeeDetector.tsx`:
- Around line 85-118: Update handleConnect to capture and store res.error when
OAuth login fails, including cases where no accessToken is returned, and
preserve clearing the error on a new attempt or successful login. Render the
stored error message in the Login tab near the connect control so the user sees
why authentication failed.
- Around line 359-374: Validate values?.apiUrl before passing it to the Link
href in the server connection UI: accept only http: and https: URLs, and use
DEFAULT_SERVER for any other value, including malformed or javascript: schemes.
Keep the existing serverHost display and link behavior unchanged for valid URLs.
---
Nitpick comments:
In `@src/background/background.ts`:
- Around line 75-77: Update the sendMessage error handler in the background
messaging flow to capture the caught error and log its reason, while preserving
the existing non-throwing behavior and undefined fallback.
In `@src/constants.ts`:
- Around line 14-25: Update OAUTH_SCOPES to include only the permissions
directly required by the in-context editor, removing destructive scopes such as
screenshots.delete and keys.edit unless their corresponding operations are
actually invoked. If those operations are needed conditionally, request their
scopes incrementally rather than in the default token scope set.
In `@src/oauth/oauthClient.ts`:
- Around line 31-39: Update the token request in the OAuth client’s fetch flow
to avoid including the raw response body in thrown errors; report only the safe
HTTP status or a generic failure message. Add an AbortSignal-based timeout to
the fetch request, using the project’s established timeout convention if
available, so user-supplied apiUrl hosts cannot stall login or refresh
indefinitely.
In `@src/oauth/tokenStore.ts`:
- Around line 7-12: Update the comment above saveSession to document that
browser.storage.local stores refresh tokens unencrypted on disk across browser
restarts as a deliberate long-lived-session trade-off versus
browser.storage.session, and state the platform’s enforced absolute
refresh-token lifetime that prevents indefinite replay of stale tokens.
In `@src/popup/sendToBackground.ts`:
- Around line 4-6: Update sendToBackground to catch rejections from
browser.runtime.sendMessage and return a normalized error result instead of
allowing the promise rejection to propagate. Preserve the existing message
payload and successful response behavior, using the helper’s return contract so
callers such as handleConnect and onLibConfigChange receive a consistent failure
result.
In `@src/popup/storage.ts`:
- Around line 25-39: Update storeValues to import and use the shared Values type
from ./tools directly, removing the local Values definition and the authToken
intersection. Preserve the existing optional values handling and storage
behavior.
In `@src/popup/useDetectorForm.tsx`:
- Around line 245-296: Update the dependency array of the project-resolution
useEffect to include libConfig?.config?.projectId and the read
checkableValues.apiUrl and checkableValues.authToken values alongside
state.credentialsCheck, so resolution reruns when any input changes.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d5e2981b-9bed-4f44-8203-5d49be78f564
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (18)
.github/workflows/test.ymlmanifest.jsonpackage.jsonsrc/background/background.tssrc/constants.tssrc/content/contentScript.tssrc/oauth/oauthClient.tssrc/oauth/pkce.tssrc/oauth/tokenStore.tssrc/popup/TolgeeDetector.tsxsrc/popup/reducer.test.tssrc/popup/reducer.tssrc/popup/sendToBackground.tssrc/popup/storage.tssrc/popup/tools.test.tssrc/popup/tools.tssrc/popup/useDetectorForm.tsxvitest.config.ts
| case 'OAUTH_GET_TOKEN': | ||
| getValidAccessToken(data.apiUrl).then((accessToken) => | ||
| sendResponse({ accessToken }) | ||
| ); | ||
| return true; | ||
| case 'OAUTH_LOGOUT': | ||
| clearSession(data.apiUrl).then(() => sendResponse({})); | ||
| return true; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add rejection handlers to OAUTH_GET_TOKEN and OAUTH_LOGOUT.
Both cases return true to keep the message channel open, but neither attaches a catch. If the promise rejects, sendResponse never runs and the caller waits on a channel that closes without a reply.
This path is reachable. keyFor in src/oauth/tokenStore.ts (Line 8) calls new URL(apiUrl), which throws a TypeError when the popup passes a malformed apiUrl. browser.storage.local calls can also reject.
The OAUTH_LOGIN case at Line 44 already handles this correctly.
🛠️ Proposed fix
case 'OAUTH_GET_TOKEN':
- getValidAccessToken(data.apiUrl).then((accessToken) =>
- sendResponse({ accessToken })
- );
+ getValidAccessToken(data.apiUrl)
+ .then((accessToken) => sendResponse({ accessToken }))
+ .catch((e) => {
+ console.error('[tolgee-oauth] token lookup failed', e);
+ sendResponse({ accessToken: null, error: String(e) });
+ });
return true;
case 'OAUTH_LOGOUT':
- clearSession(data.apiUrl).then(() => sendResponse({}));
+ clearSession(data.apiUrl)
+ .then(() => sendResponse({}))
+ .catch((e) => {
+ console.error('[tolgee-oauth] logout failed', e);
+ sendResponse({ error: String(e) });
+ });
return true;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case 'OAUTH_GET_TOKEN': | |
| getValidAccessToken(data.apiUrl).then((accessToken) => | |
| sendResponse({ accessToken }) | |
| ); | |
| return true; | |
| case 'OAUTH_LOGOUT': | |
| clearSession(data.apiUrl).then(() => sendResponse({})); | |
| return true; | |
| case 'OAUTH_GET_TOKEN': | |
| getValidAccessToken(data.apiUrl) | |
| .then((accessToken) => sendResponse({ accessToken })) | |
| .catch((e) => { | |
| console.error('[tolgee-oauth] token lookup failed', e); | |
| sendResponse({ accessToken: null, error: String(e) }); | |
| }); | |
| return true; | |
| case 'OAUTH_LOGOUT': | |
| clearSession(data.apiUrl) | |
| .then(() => sendResponse({})) | |
| .catch((e) => { | |
| console.error('[tolgee-oauth] logout failed', e); | |
| sendResponse({ error: String(e) }); | |
| }); | |
| return true; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/background/background.ts` around lines 49 - 56, Update the
OAUTH_GET_TOKEN and OAUTH_LOGOUT handlers to attach rejection handlers to their
promises, ensuring sendResponse is invoked with an error response when
getValidAccessToken or clearSession rejects, including malformed apiUrl and
storage failures. Preserve the existing success responses and return true
behavior, matching the error-handling pattern used by OAUTH_LOGIN.
| // Keep stored sessions fresh so the popup and the injected page token don't expire mid-use. Rotation means each | ||
| // refresh mints a new access + refresh token; getValidAccessToken persists them and pushes the access token to tabs. | ||
| browser.alarms.create(REFRESH_ALARM, { periodInMinutes: 10 }); | ||
| browser.alarms.onAlarm.addListener(async (alarm) => { | ||
| if (alarm.name !== REFRESH_ALARM) { | ||
| return; | ||
| } | ||
| const sessions = await loadAllSessions(); | ||
| for (const session of sessions) { | ||
| if (session.expiresAt - OAUTH_REFRESH_SKEW_MS > Date.now()) { | ||
| continue; | ||
| } | ||
| const accessToken = await getValidAccessToken(session.apiUrl); | ||
| if (accessToken) { | ||
| await pushTokenToTabs(session.apiUrl, accessToken); | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Create the alarm only when it does not already exist.
Line 82 runs at the top level of the service worker and calls browser.alarms.create unconditionally. An MV3 service worker restarts after about 30 seconds of idle time, so this line re-runs on every wake. Creating an alarm with an existing name replaces it and restarts the 10-minute schedule.
If the extension receives events more often than every 10 minutes, the alarm never reaches its fire time, and stored sessions never refresh. The Chrome alarms documentation recommends checking for the alarm on each service worker start and creating it only when it is absent. The docs state it is best to make sure important alarms exist each time your service worker starts up, using chrome.alarms.get before create.
Keep the onAlarm listener registration at the top level. That part is correct.
🛠️ Proposed fix
-browser.alarms.create(REFRESH_ALARM, { periodInMinutes: 10 });
+const ensureRefreshAlarm = async () => {
+ const existing = await browser.alarms.get(REFRESH_ALARM);
+ if (!existing) {
+ await browser.alarms.create(REFRESH_ALARM, { periodInMinutes: 10 });
+ }
+};
+ensureRefreshAlarm();
+
browser.alarms.onAlarm.addListener(async (alarm) => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Keep stored sessions fresh so the popup and the injected page token don't expire mid-use. Rotation means each | |
| // refresh mints a new access + refresh token; getValidAccessToken persists them and pushes the access token to tabs. | |
| browser.alarms.create(REFRESH_ALARM, { periodInMinutes: 10 }); | |
| browser.alarms.onAlarm.addListener(async (alarm) => { | |
| if (alarm.name !== REFRESH_ALARM) { | |
| return; | |
| } | |
| const sessions = await loadAllSessions(); | |
| for (const session of sessions) { | |
| if (session.expiresAt - OAUTH_REFRESH_SKEW_MS > Date.now()) { | |
| continue; | |
| } | |
| const accessToken = await getValidAccessToken(session.apiUrl); | |
| if (accessToken) { | |
| await pushTokenToTabs(session.apiUrl, accessToken); | |
| } | |
| } | |
| }); | |
| // Keep stored sessions fresh so the popup and the injected page token don't expire mid-use. Rotation means each | |
| // refresh mints a new access + refresh token; getValidAccessToken persists them and pushes the access token to tabs. | |
| const ensureRefreshAlarm = async () => { | |
| const existing = await browser.alarms.get(REFRESH_ALARM); | |
| if (!existing) { | |
| await browser.alarms.create(REFRESH_ALARM, { periodInMinutes: 10 }); | |
| } | |
| }; | |
| ensureRefreshAlarm(); | |
| browser.alarms.onAlarm.addListener(async (alarm) => { | |
| if (alarm.name !== REFRESH_ALARM) { | |
| return; | |
| } | |
| const sessions = await loadAllSessions(); | |
| for (const session of sessions) { | |
| if (session.expiresAt - OAUTH_REFRESH_SKEW_MS > Date.now()) { | |
| continue; | |
| } | |
| const accessToken = await getValidAccessToken(session.apiUrl); | |
| if (accessToken) { | |
| await pushTokenToTabs(session.apiUrl, accessToken); | |
| } | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/background/background.ts` around lines 80 - 97, Update the top-level
alarm setup around REFRESH_ALARM to first check whether the alarm already
exists, creating it with the 10-minute period only when absent. Keep the
browser.alarms.onAlarm listener registration at the top level and preserve its
existing refresh handling.
| // Background pushes a rotated access token here on refresh; update it in place so the SDK picks it up without a reload. | ||
| messages.listenRuntime('UPDATE_AUTH_TOKEN', async (data) => { | ||
| if (sameOrigin(sessionStorage.getItem(API_URL_LOCAL_STORAGE), data.apiUrl)) { | ||
| sessionStorage.setItem(AUTH_TOKEN_LOCAL_STORAGE, data.authToken); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Guard UPDATE_AUTH_TOKEN against a missing token.
If data.authToken is undefined, sessionStorage.setItem stores the literal string "undefined". The SDK then sends Bearer undefined and in-context editing fails until the page reloads. Reject empty values before the write.
🛡️ Proposed guard
messages.listenRuntime('UPDATE_AUTH_TOKEN', async (data) => {
- if (sameOrigin(sessionStorage.getItem(API_URL_LOCAL_STORAGE), data.apiUrl)) {
+ if (
+ data.authToken &&
+ sameOrigin(sessionStorage.getItem(API_URL_LOCAL_STORAGE), data.apiUrl)
+ ) {
sessionStorage.setItem(AUTH_TOKEN_LOCAL_STORAGE, data.authToken);
}
});Run the following script to confirm the background always sends a token with this message:
#!/bin/bash
rg -n -C6 'UPDATE_AUTH_TOKEN' --glob '*.ts' --glob '*.tsx'🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/content/contentScript.ts` around lines 112 - 117, Update the
UPDATE_AUTH_TOKEN listener to validate that data.authToken is present and
non-empty before calling sessionStorage.setItem; retain the same-origin check
and skip the write when the token is missing so an invalid "undefined" value
cannot reach the SDK.
| const parseTokenResponse = ( | ||
| data: Record<string, any>, | ||
| previousRefreshToken?: string | ||
| ): OAuthTokens => ({ | ||
| accessToken: data.access_token, | ||
| // rotation returns a fresh refresh token; if a response omits it, keep the previous one | ||
| refreshToken: data.refresh_token ?? previousRefreshToken, | ||
| expiresAt: Date.now() + (data.expires_in ?? 0) * 1000, | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Validate the token response before building OAuthTokens.
Two gaps exist in parseTokenResponse:
- If
access_tokenis absent,accessTokenbecomesundefined. The session is then saved and injected into the page as an invalid token, and the failure surfaces later as an opaque 401. - If
expires_inis absent,expiresAtequalsDate.now().getValidAccessTokenthen treats the session as expired on every call and triggers a refresh each time. With refresh-token rotation, this produces a refresh request per token read.
Reject a response without access_token, and use a conservative default lifetime when expires_in is absent.
🛠️ Proposed fix
const parseTokenResponse = (
data: Record<string, any>,
previousRefreshToken?: string
-): OAuthTokens => ({
- accessToken: data.access_token,
- // rotation returns a fresh refresh token; if a response omits it, keep the previous one
- refreshToken: data.refresh_token ?? previousRefreshToken,
- expiresAt: Date.now() + (data.expires_in ?? 0) * 1000,
-});
+): OAuthTokens => {
+ if (typeof data.access_token !== 'string' || !data.access_token) {
+ throw new Error('Tolgee token endpoint returned no access_token');
+ }
+ const expiresIn =
+ typeof data.expires_in === 'number' && data.expires_in > 0
+ ? data.expires_in
+ : DEFAULT_TOKEN_LIFETIME_SECONDS;
+ return {
+ accessToken: data.access_token,
+ // rotation returns a fresh refresh token; if a response omits it, keep the previous one
+ refreshToken: data.refresh_token ?? previousRefreshToken,
+ expiresAt: Date.now() + expiresIn * 1000,
+ };
+};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/oauth/oauthClient.ts` around lines 16 - 24, Update parseTokenResponse to
validate that data.access_token is present and reject the response before
constructing OAuthTokens when it is missing. Replace the zero-second expires_in
fallback with a conservative default lifetime so getValidAccessToken does not
treat tokens with omitted expiry as immediately expired; preserve the existing
refresh-token fallback behavior.
| authorizeUrl.searchParams.set('state', randomUrlSafe()); | ||
| if (projectId != null) { | ||
| authorizeUrl.searchParams.set('project', String(projectId)); | ||
| } | ||
|
|
||
| const redirectResponse = await browser.identity.launchWebAuthFlow({ | ||
| url: authorizeUrl.toString(), | ||
| interactive: true, | ||
| }); | ||
| const code = new URL(redirectResponse).searchParams.get('code'); | ||
| if (!code) { | ||
| throw new Error('Tolgee authorization did not return a code'); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Validate the state parameter in the redirect response.
Line 61 generates state and sends it, but the code never compares it with the value returned on the redirect. The control has no effect as written. PKCE and the extension-owned redirect URI limit the practical impact, but OAuth 2.1 requires the client to reject a response whose state does not match the request.
The redirect can also carry error and error_description instead of code. Surface that reason so the popup can show why the login failed.
🛠️ Proposed fix
- authorizeUrl.searchParams.set('state', randomUrlSafe());
+ const state = randomUrlSafe();
+ authorizeUrl.searchParams.set('state', state);
if (projectId != null) {
authorizeUrl.searchParams.set('project', String(projectId));
}
const redirectResponse = await browser.identity.launchWebAuthFlow({
url: authorizeUrl.toString(),
interactive: true,
});
- const code = new URL(redirectResponse).searchParams.get('code');
+ const redirectParams = new URL(redirectResponse).searchParams;
+ if (redirectParams.get('state') !== state) {
+ throw new Error('Tolgee authorization returned an unexpected state');
+ }
+ const error = redirectParams.get('error');
+ if (error) {
+ throw new Error(`Tolgee authorization failed: ${error}`);
+ }
+ const code = redirectParams.get('code');
if (!code) {
throw new Error('Tolgee authorization did not return a code');
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| authorizeUrl.searchParams.set('state', randomUrlSafe()); | |
| if (projectId != null) { | |
| authorizeUrl.searchParams.set('project', String(projectId)); | |
| } | |
| const redirectResponse = await browser.identity.launchWebAuthFlow({ | |
| url: authorizeUrl.toString(), | |
| interactive: true, | |
| }); | |
| const code = new URL(redirectResponse).searchParams.get('code'); | |
| if (!code) { | |
| throw new Error('Tolgee authorization did not return a code'); | |
| } | |
| const state = randomUrlSafe(); | |
| authorizeUrl.searchParams.set('state', state); | |
| if (projectId != null) { | |
| authorizeUrl.searchParams.set('project', String(projectId)); | |
| } | |
| const redirectResponse = await browser.identity.launchWebAuthFlow({ | |
| url: authorizeUrl.toString(), | |
| interactive: true, | |
| }); | |
| const redirectParams = new URL(redirectResponse).searchParams; | |
| if (redirectParams.get('state') !== state) { | |
| throw new Error('Tolgee authorization returned an unexpected state'); | |
| } | |
| const error = redirectParams.get('error'); | |
| if (error) { | |
| throw new Error(`Tolgee authorization failed: ${error}`); | |
| } | |
| const code = redirectParams.get('code'); | |
| if (!code) { | |
| throw new Error('Tolgee authorization did not return a code'); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/oauth/oauthClient.ts` around lines 61 - 73, Update the authorization flow
around authorizeUrl and redirectResponse to retain the generated state, parse
the redirect URL once, and reject responses whose returned state differs or is
missing. Before requiring code, detect redirect error and error_description
parameters and throw an error that includes the provider’s failure reason;
preserve the existing missing-code validation for responses without either a
code or provider error.
| export const getValidAccessToken = async ( | ||
| apiUrl: string | ||
| ): Promise<string | null> => { | ||
| const session = await loadSession(apiUrl); | ||
| if (!session) { | ||
| return null; | ||
| } | ||
| if (session.expiresAt - OAUTH_REFRESH_SKEW_MS > Date.now()) { | ||
| return session.accessToken; | ||
| } | ||
| if (!session.refreshToken) { | ||
| await clearSession(apiUrl); | ||
| return null; | ||
| } | ||
| try { | ||
| const refreshed = await refresh(apiUrl, session.refreshToken); | ||
| await saveSession(apiUrl, refreshed); | ||
| return refreshed.accessToken; | ||
| } catch (e) { | ||
| await clearSession(apiUrl); | ||
| return null; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Deduplicate concurrent refreshes for the same apiUrl.
getValidAccessToken reads the session, then refreshes, with no lock. Two overlapping calls for the same apiUrl both read the same refreshToken and both POST to /oauth2/token.
The refresh flow rotates tokens, as stated at Line 22 of src/oauth/oauthClient.ts. The first response invalidates the refresh token. The second request then fails, the catch at Line 53 runs, and clearSession deletes the session that was just refreshed. The user is logged out silently.
This path is reachable. The alarm handler at Line 92 of src/background/background.ts calls getValidAccessToken, and an OAUTH_GET_TOKEN message can arrive in the same service worker at the same time.
Cache the in-flight refresh promise per apiUrl, and clear the session only for authentication failures rather than for every error.
🛠️ Proposed fix
+const inFlight = new Map<string, Promise<string | null>>();
+
export const getValidAccessToken = async (
apiUrl: string
): Promise<string | null> => {
const session = await loadSession(apiUrl);
if (!session) {
return null;
}
if (session.expiresAt - OAUTH_REFRESH_SKEW_MS > Date.now()) {
return session.accessToken;
}
if (!session.refreshToken) {
await clearSession(apiUrl);
return null;
}
- try {
- const refreshed = await refresh(apiUrl, session.refreshToken);
- await saveSession(apiUrl, refreshed);
- return refreshed.accessToken;
- } catch (e) {
- await clearSession(apiUrl);
- return null;
- }
+ const key = keyFor(apiUrl);
+ const existing = inFlight.get(key);
+ if (existing) {
+ return existing;
+ }
+ const pending = (async () => {
+ try {
+ const refreshed = await refresh(apiUrl, session.refreshToken!);
+ await saveSession(apiUrl, refreshed);
+ return refreshed.accessToken;
+ } catch (e) {
+ await clearSession(apiUrl);
+ return null;
+ } finally {
+ inFlight.delete(key);
+ }
+ })();
+ inFlight.set(key, pending);
+ return pending;
};Note: the service worker can terminate between calls, so this map reduces but does not eliminate the race. Server-side refresh-token replay tolerance, or a single-flight lock in browser.storage, closes the remaining window.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/oauth/tokenStore.ts` around lines 35 - 57, Update getValidAccessToken to
deduplicate concurrent refreshes by caching one in-flight refresh promise per
apiUrl, reusing it for overlapping callers, and removing it when settled.
Preserve existing token/session behavior, but only clear the session when
refresh reports an authentication failure; propagate or retain the session for
other errors instead of treating every failure as invalid credentials.
| case 'APPLY_VALUES': { | ||
| // sync values with storage/localStorage | ||
| apply(); | ||
| const branchEnabled = | ||
| state.credentialsCheck !== null && | ||
| typeof state.credentialsCheck === 'object' && | ||
| 'branchingEnabled' in state.credentialsCheck && | ||
| state.credentialsCheck.branchingEnabled; | ||
| const effectiveBranch = branchEnabled | ||
| ? state.values?.branch | ||
| : undefined; | ||
| return { | ||
| ...state, | ||
| appliedValues: { | ||
| apiKey: state.values?.apiKey, | ||
| apiUrl: state.values?.apiUrl, | ||
| branch: effectiveBranch, | ||
| }, | ||
| storedValues: { | ||
| apiKey: state.values?.apiKey, | ||
| apiUrl: state.values?.apiUrl, | ||
| branch: effectiveBranch, | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
APPLY_VALUES drops the OAuth session fields.
APPLY_VALUES rebuilds appliedValues and storedValues from apiKey, apiUrl, and branch only. It discards authToken and projectId. This action is reachable in the OAuth path: handleKeyDown in src/popup/TolgeeDetector.tsx (Line 78-83) dispatches APPLY_VALUES when validateValues(values) passes, and validateValues now accepts authToken without apiKey. The Server field carries onKeyDown={handleKeyDown} and is rendered on the Login tab when the user opens "Change server". Pressing Enter there clears the token from state, pushes empty credentials to the page through SET_CREDENTIALS, and makes storeValues remove the stored OAuth entry.
Preserve the OAuth fields in this action.
🐛 Proposed fix
const effectiveBranch = branchEnabled
? state.values?.branch
: undefined;
+ const nextValues = {
+ apiKey: state.values?.apiKey,
+ apiUrl: state.values?.apiUrl,
+ branch: effectiveBranch,
+ authToken: state.values?.authToken,
+ projectId: state.values?.projectId,
+ };
return {
...state,
- appliedValues: {
- apiKey: state.values?.apiKey,
- apiUrl: state.values?.apiUrl,
- branch: effectiveBranch,
- },
- storedValues: {
- apiKey: state.values?.apiKey,
- apiUrl: state.values?.apiUrl,
- branch: effectiveBranch,
- },
+ appliedValues: nextValues,
+ storedValues: nextValues,
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case 'APPLY_VALUES': { | |
| // sync values with storage/localStorage | |
| apply(); | |
| const branchEnabled = | |
| state.credentialsCheck !== null && | |
| typeof state.credentialsCheck === 'object' && | |
| 'branchingEnabled' in state.credentialsCheck && | |
| state.credentialsCheck.branchingEnabled; | |
| const effectiveBranch = branchEnabled | |
| ? state.values?.branch | |
| : undefined; | |
| return { | |
| ...state, | |
| appliedValues: { | |
| apiKey: state.values?.apiKey, | |
| apiUrl: state.values?.apiUrl, | |
| branch: effectiveBranch, | |
| }, | |
| storedValues: { | |
| apiKey: state.values?.apiKey, | |
| apiUrl: state.values?.apiUrl, | |
| branch: effectiveBranch, | |
| }, | |
| }; | |
| } | |
| case 'APPLY_VALUES': { | |
| // sync values with storage/localStorage | |
| apply(); | |
| const branchEnabled = | |
| state.credentialsCheck !== null && | |
| typeof state.credentialsCheck === 'object' && | |
| 'branchingEnabled' in state.credentialsCheck && | |
| state.credentialsCheck.branchingEnabled; | |
| const effectiveBranch = branchEnabled | |
| ? state.values?.branch | |
| : undefined; | |
| const nextValues = { | |
| apiKey: state.values?.apiKey, | |
| apiUrl: state.values?.apiUrl, | |
| branch: effectiveBranch, | |
| authToken: state.values?.authToken, | |
| projectId: state.values?.projectId, | |
| }; | |
| return { | |
| ...state, | |
| appliedValues: nextValues, | |
| storedValues: nextValues, | |
| }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/popup/reducer.ts` around lines 131 - 155, Update the APPLY_VALUES case in
the reducer so appliedValues and storedValues preserve the existing OAuth
authToken and projectId alongside apiKey, apiUrl, and the conditionally
effective branch. Ensure the OAuth values remain intact when APPLY_VALUES is
triggered through the Server field without changing the existing branchEnabled
behavior.
| const handleConnect = async () => { | ||
| const apiUrl = values?.apiUrl || DEFAULT_SERVER; | ||
| setConnecting(true); | ||
| try { | ||
| // Hint the project the page is configured for (exposed via the handshake), so the consent screen pre-selects it | ||
| // and the minted token is scoped to it. On a public project the hint resolves via the community floor. | ||
| const hinted = (libConfig?.config as { projectId?: number | string }) | ||
| ?.projectId; | ||
| const projectId = | ||
| hinted !== undefined && hinted !== '' ? Number(hinted) : undefined; | ||
| // Capture the target tab now: launchWebAuthFlow closes the popup, so the background does the injection and needs | ||
| // the tab id up front. | ||
| const [activeTab] = await browser.tabs.query({ | ||
| active: true, | ||
| currentWindow: true, | ||
| }); | ||
| const res = (await sendToBackground('OAUTH_LOGIN', { | ||
| apiUrl, | ||
| projectId, | ||
| tabId: activeTab?.id, | ||
| })) as { | ||
| accessToken?: string; | ||
| error?: string; | ||
| }; | ||
| if (res?.accessToken) { | ||
| dispatch({ | ||
| type: 'OAUTH_APPLY', | ||
| payload: { apiUrl, authToken: res.accessToken }, | ||
| }); | ||
| } | ||
| } finally { | ||
| setConnecting(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Surface the login error to the user.
The response type declares error, but the handler ignores it. If the OAuth flow fails, for example when the user closes the consent window or the token exchange fails, the button returns to its idle label and the popup shows no reason. Store the error and render it in the Login tab.
🐛 Proposed change
+ const [connectError, setConnectError] = useState<string | null>(null);
...
const handleConnect = async () => {
const apiUrl = values?.apiUrl || DEFAULT_SERVER;
setConnecting(true);
+ setConnectError(null);
try {
...
if (res?.accessToken) {
dispatch({
type: 'OAUTH_APPLY',
payload: { apiUrl, authToken: res.accessToken },
});
+ } else {
+ setConnectError(res?.error || 'Connection failed');
}
} finally {
setConnecting(false);
}
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/popup/TolgeeDetector.tsx` around lines 85 - 118, Update handleConnect to
capture and store res.error when OAuth login fails, including cases where no
accessToken is returned, and preserve clearing the error on a new attempt or
successful login. Render the stored error message in the Login tab near the
connect control so the user sees why authentication failed.
| {serverOpen ? ( | ||
| serverField | ||
| ) : ( | ||
| <Typography variant="body2"> | ||
| Connect to your account on{' '} | ||
| <Link | ||
| href={values?.apiUrl || DEFAULT_SERVER} | ||
| target="_blank" | ||
| rel="noreferrer" | ||
| underline="hover" | ||
| > | ||
| {serverHost} | ||
| </Link>{' '} | ||
| and start translating. | ||
| </Typography> | ||
| )} |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Validate the scheme before you use the server value as a link target.
values?.apiUrl comes from the editable Server field and goes straight into href. A value such as javascript:... produces an executable link inside the extension popup, which runs with extension privileges. Restrict the href to http: and https:, and fall back to DEFAULT_SERVER.
🛡️ Proposed guard
+ let serverLink = DEFAULT_SERVER;
+ try {
+ const parsed = new URL(values?.apiUrl || DEFAULT_SERVER);
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
+ serverLink = parsed.toString();
+ }
+ } catch {
+ // keep the default when the value is not a full URL yet
+ }
...
<Link
- href={values?.apiUrl || DEFAULT_SERVER}
+ href={serverLink}
target="_blank"
rel="noreferrer"
underline="hover"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {serverOpen ? ( | |
| serverField | |
| ) : ( | |
| <Typography variant="body2"> | |
| Connect to your account on{' '} | |
| <Link | |
| href={values?.apiUrl || DEFAULT_SERVER} | |
| target="_blank" | |
| rel="noreferrer" | |
| underline="hover" | |
| > | |
| {serverHost} | |
| </Link>{' '} | |
| and start translating. | |
| </Typography> | |
| )} | |
| {serverOpen ? ( | |
| serverField | |
| ) : ( | |
| <Typography variant="body2"> | |
| Connect to your account on{' '} | |
| <Link | |
| href={serverLink} | |
| target="_blank" | |
| rel="noreferrer" | |
| underline="hover" | |
| > | |
| {serverHost} | |
| </Link>{' '} | |
| and start translating. | |
| </Typography> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/popup/TolgeeDetector.tsx` around lines 359 - 374, Validate values?.apiUrl
before passing it to the Link href in the server connection UI: accept only
http: and https: URLs, and use DEFAULT_SERVER for any other value, including
malformed or javascript: schemes. Keep the existing serverHost display and link
behavior unchanged for valid URLs.
Part of the cross-repo OAuth 2.1 work (see tolgee/tolgee-platform#3849). Adds a browser-redirect OAuth login so a contributor can authorize the extension with their own access instead of pasting a Project API Key.
Changes
chrome.identity.launchWebAuthFlow+ PKCE.Draft — depends on the platform and tolgee-js branches of the same name.
Summary by CodeRabbit
New Features
Bug Fixes
Tests